home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / apport / chroot.py < prev    next >
Encoding:
Python Source  |  2009-04-06  |  15.4 KB  |  459 lines

  1. '''Class for representing and working with chroots.'''
  2.  
  3. # (c) 2007, 2008 Canonical Ltd.
  4. # Author: Martin Pitt <martin.pitt@ubuntu.com>
  5. #
  6. # This program is free software; you can redistribute it and/or modify it
  7. # under the terms of the GNU General Public License as published by the
  8. # Free Software Foundation; either version 2 of the License, or (at your
  9. # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
  10. # the full text of the license.
  11.  
  12. import subprocess, tempfile, os, os.path, shutil
  13.  
  14. def setup_fakeroot_env():
  15.     '''Set up a fakeroot/fakechroot environment.
  16.  
  17.     This needs the libraries /usr/lib/libfakeroot/libfakeroot-sysv.so and
  18.     /usr/lib/fakechroot/libfakechroot.so; these paths can be overridden
  19.     with the environment variables APPORT_LIBFAKEROOT and
  20.     APPORT_LIBFAKECHROOT.'''
  21.  
  22.     libfakeroot = os.environ.get('APPORT_LIBFAKEROOT',
  23.         '/usr/lib/libfakeroot/libfakeroot-sysv.so')
  24.     assert os.path.exists(libfakeroot), \
  25.         '%s not found; please set APPORT_LIBFAKEROOT correctly' % libfakeroot
  26.  
  27.     libfakechroot = os.environ.get('APPORT_LIBFAKECHROOT',
  28.         '/usr/lib/fakechroot/libfakechroot.so')
  29.     assert os.path.exists(libfakechroot), \
  30.         '%s not found; please set APPORT_LIBFAKECHROOT correctly' % libfakechroot
  31.  
  32.     os.environ['LD_PRELOAD'] = '%s %s %s' % (libfakechroot, libfakeroot,
  33.         os.environ.get('LD_PRELOAD', ''))
  34.     os.environ['LD_LIBRARY_PATH'] = '/lib:' + os.environ.get('LD_LIBRARY_PATH', '')
  35.     os.environ['FAKECHROOT'] = 'true'
  36.  
  37. def pathsplit(p, rest=[]):
  38.     (h,t) = os.path.split(p)
  39.     if len(h) < 1: return [t]+rest
  40.     if len(t) < 1: return [h]+rest
  41.     return pathsplit(h,[t]+rest)
  42.  
  43. def commonpath(l1, l2, common=[]):
  44.     if len(l1) < 1: return (common, l1, l2)
  45.     if len(l2) < 1: return (common, l1, l2)
  46.     if l1[0] != l2[0]: return (common, l1, l2)
  47.     return commonpath(l1[1:], l2[1:], common+[l1[0]])
  48.  
  49. def relpath(p1, p2):
  50.     (common,l1,l2) = commonpath(pathsplit(p1), pathsplit(p2))
  51.     p = []
  52.     if len(l1) > 0:
  53.         p = [ '../' * len(l1) ]
  54.     p = p + l2
  55.     return os.path.join( *p )
  56.  
  57. class Chroot:
  58.     '''Work with a chroot (either in directory or in tarball form).
  59.  
  60.     If called as non-root user, this calls setup_fakeroot_env() to use the
  61.     fakeroot/fakechroot libraries.'''
  62.  
  63.     def __init__(self, root):
  64.         '''Bind to a chroot, which can either be a directory, a tarball, or
  65.         None to work in the main system.
  66.  
  67.         If a tarball is given, then it gets unpacked into a temporary directory
  68.         which is cleaned up at program termination.'''
  69.  
  70.         self.remove = False
  71.  
  72.         if os.geteuid() != 0:
  73.             setup_fakeroot_env()
  74.  
  75.         self.root_tarball = None
  76.         if root is None:
  77.             self.root = None
  78.         elif os.path.isdir(root):
  79.             self.root = root
  80.         else:
  81.             assert os.path.isfile(root)
  82.             self.root_tarball = root
  83.             self.root = tempfile.mkdtemp()
  84.             self.remove = True
  85.             assert subprocess.call(['tar', '-C', self.root,
  86.                 '-xzf', root]) == 0
  87.  
  88.     def __del__(self):
  89.         if hasattr(self, 'remove') and self.remove:
  90.             shutil.rmtree(self.root)
  91.  
  92.     def tar(self, tarball=None):
  93.         '''Create a tarball from the chroot.
  94.  
  95.         If tarball does not specify a .tar.gz path, then the Chroot must have
  96.         been created from a tarball, and that tarball is updated.'''
  97.  
  98.         if not tarball:
  99.             assert self.root_tarball
  100.             tarball = self.root_tarball
  101.  
  102.         f = open(tarball, 'w')
  103.  
  104.         orig_cwd = os.getcwd()
  105.         try:
  106.             os.chdir(self.root)
  107.             tar = subprocess.Popen(['tar', 'cp', '.'], stdout=subprocess.PIPE)
  108.             gz = subprocess.Popen(['gzip', '-9'], stdin=tar.stdout, stdout=f)
  109.             assert tar.wait() == 0
  110.             assert gz.wait() == 0
  111.             f.close()
  112.         finally:
  113.             os.chdir(orig_cwd)
  114.  
  115.     def _exec_capture(self, argv, stdin=None):
  116.         '''Internal helper function to wrap subprocess.Popen() and return a
  117.         triple (stdout, stderr, returncode).'''
  118.  
  119.         if stdin:
  120.             p = subprocess.Popen(argv, stdin=subprocess.PIPE,
  121.                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  122.             (out, err) = p.communicate(stdin)
  123.         else:
  124.             p = subprocess.Popen(argv, stdin=subprocess.PIPE,
  125.                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  126.             (out, err) = p.communicate()
  127.         return (out, err, p.returncode)
  128.  
  129.     def run(self, argv):
  130.         '''Execute the given commandline vector in the chroot and return the
  131.         exit code.'''
  132.  
  133.         if self.root:
  134.             return subprocess.call(['chroot', self.root] + argv)
  135.         else:
  136.             return subprocess.call(argv)
  137.  
  138.     def run_capture(self, argv, stdin=None):
  139.         '''Execute the given command line vector in the chroot and return a
  140.         triple (stdout, stderr, exit code).'''
  141.  
  142.         if self.root:
  143.             return self._exec_capture(['chroot', self.root] +
  144.                argv, stdin)
  145.         else:
  146.            return self._exec_capture(argv, stdin)
  147.  
  148.     def fix_symlinks(self):
  149.         '''Remove root prefix from symbolic links in chroot directory,
  150.         otherwise chroot tarballs don't work at all, and we cannot move/rename
  151.         chroot directories.'''
  152.  
  153.         for r, dirs, files in os.walk(self.root):
  154.             for f in files:
  155.                 path = os.path.join(r, f)
  156.                 if os.path.islink(path):
  157.                     target = os.readlink(path)
  158.                     if target.startswith(self.root):
  159.                         os.unlink(path)
  160.                         os.symlink(relpath(os.path.dirname(path), target), path)
  161.  
  162. #
  163. # Unit test
  164. #
  165.  
  166. if __name__ == '__main__':
  167.     import unittest, os, tarfile, re, shutil
  168.  
  169.     class ChrootTest(unittest.TestCase):
  170.         def test_null(self):
  171.             '''null chroot (working in the main system)'''
  172.  
  173.             c = Chroot(None)
  174.             self.assertEqual(c.root_tarball, None)
  175.             self.assertEqual(c.run(['/bin/sh', '-c', 'exit 42']), 42)
  176.  
  177.             (out, err, ret) = c.run_capture(['/bin/ls', '/bin/ls'])
  178.             self.assertEqual(ret, 0)
  179.             self.assertEqual(out, '/bin/ls\n')
  180.             self.assertEqual(err, '')
  181.  
  182.             (out, err, ret) = c.run_capture(['/bin/ls', '/nonexisting/gibberish'])
  183.             self.assertNotEqual(ret, 0)
  184.             self.assertEqual(out, '')
  185.             self.assertNotEqual(err, '')
  186.  
  187.         def _mkchroot(self):
  188.             '''Return a test chroot dir with /bin/hello and /bin/42.'''
  189.  
  190.             d = tempfile.mkdtemp()
  191.             bindir = os.path.join(d, 'bin')
  192.             os.mkdir(bindir)
  193.             open(os.path.join(d, 'hello.c'), 'w').write('''
  194. #include <stdio.h>
  195. int main() { puts("hello"); return 0; }
  196. ''')
  197.             open(os.path.join(d, '42.c'), 'w').write('''
  198. int main() { return 42; }
  199. ''')
  200.             assert subprocess.call(['cc', '-static', os.path.join(d,
  201.                 'hello.c'), '-o', os.path.join(bindir, 'hello')]) == 0
  202.             assert subprocess.call(['cc', '-static', os.path.join(d,
  203.                 '42.c'), '-o', os.path.join(bindir, '42')]) == 0
  204.  
  205.             return d
  206.  
  207.         def test_dir(self):
  208.             '''directory chroot.'''
  209.  
  210.             d = self._mkchroot()
  211.             tarpath = None
  212.             try:
  213.                 c = Chroot(d)
  214.                 self.assertEqual(c.root_tarball, None)
  215.  
  216.                 # test running
  217.                 self.assertEqual(c.run(['/bin/42']), 42)
  218.                 (out, err, ret) = c.run_capture(['/bin/hello'])
  219.                 self.assertEqual(ret, 0)
  220.                 self.assertEqual(out, 'hello\n')
  221.                 self.assertEqual(err, '')
  222.  
  223.                 # test tar'ing
  224.                 open(os.path.join(c.root, 'newfile'), 'w')
  225.                 self.assertRaises(AssertionError, c.tar)
  226.                 (fd, tarpath) = tempfile.mkstemp()
  227.                 os.close(fd)
  228.                 c.tar(tarpath)
  229.                 t = tarfile.open(tarpath)
  230.                 self.assert_(
  231.                     # python 2.5's tarfile
  232.                     set(['./bin/42', './bin/hello', './newfile']).issubset(set(t.getnames())) or
  233.                     # python 2.4's tarfile
  234.                     set(['bin/42', 'bin/hello', 'newfile']).issubset(set(t.getnames()))
  235.                 )
  236.  
  237.                 # test cleanup
  238.                 del c
  239.                 self.assert_(os.path.exists(os.path.join(d, 'bin', '42')),
  240.                     'directory chroot should not delete the chroot')
  241.  
  242.             finally:
  243.                 shutil.rmtree(d)
  244.                 if tarpath:
  245.                     os.unlink(tarpath)
  246.  
  247.         def test_tarball(self):
  248.             '''tarball chroot.'''
  249.  
  250.             d = self._mkchroot()
  251.             try:
  252.                 (fd, tar) = tempfile.mkstemp()
  253.                 os.close(fd)
  254.                 orig_cwd = os.getcwd()
  255.                 os.chdir(d)
  256.                 assert subprocess.call(['tar', 'czPf', tar, '.']) == 0
  257.                 assert os.path.exists(tar)
  258.                 os.chdir(orig_cwd)
  259.             finally:
  260.                 shutil.rmtree(d)
  261.  
  262.             try:
  263.                 c = Chroot(tar)
  264.                 self.assertEqual(c.root_tarball, tar)
  265.  
  266.                 # test running
  267.                 self.assertEqual(c.run(['/bin/42']), 42)
  268.                 (out, err, ret) = c.run_capture(['/bin/hello'])
  269.                 self.assertEqual(ret, 0)
  270.                 self.assertEqual(out, 'hello\n')
  271.                 self.assertEqual(err, '')
  272.  
  273.                 # test tar'ing
  274.                 open(os.path.join(c.root, 'newfile'), 'w')
  275.                 c.tar()
  276.                 t = tarfile.open(tar)
  277.                 self.assert_(
  278.                     # python 2.5's tarfile
  279.                     set(['./bin/42', './bin/hello', './newfile']).issubset(set(t.getnames())) or
  280.                     # python 2.4's tarfile
  281.                     set(['bin/42', 'bin/hello', 'newfile']).issubset(set(t.getnames()))
  282.                 )
  283.  
  284.                 # test cleanup
  285.                 d = c.root
  286.                 del c
  287.                 self.assert_(not os.path.exists(d),
  288.                     'tarball chroot should delete the temporary chroot')
  289.             finally:
  290.                 os.unlink(tar)
  291.  
  292.         def test_fix_symlinks(self):
  293.             '''symlink fixing in chroots.'''
  294.  
  295.             d = self._mkchroot()
  296.             try:
  297.                 os.symlink(os.path.join(d, 'bin', '42'), os.path.join(d, 'bin', '42prefix'))
  298.                 os.symlink(os.path.join('/bin/42'), os.path.join(d, 'bin', '42noprefix'))
  299.                 os.symlink(os.path.join('bin/42'), os.path.join(d, '42rel'))
  300.  
  301.                 c = Chroot(d)
  302.                 c.fix_symlinks()
  303.  
  304.                 self.assertEqual(c.run(['/42rel']), 42)
  305.                 self.assertEqual(c.run(['/bin/42prefix']), 42)
  306.                 self.assertEqual(os.readlink(os.path.join(d, 'bin', '42noprefix')), '/bin/42')
  307.  
  308.             finally:
  309.                 shutil.rmtree(d)
  310.  
  311.         @classmethod
  312.         def _install_file(klass, path, root):
  313.             '''Install given file into a chroot, preserving the path.
  314.             
  315.             Do nothing if the target file already exists.'''
  316.  
  317.             destpath = root + os.path.abspath(path)
  318.             if os.path.exists(destpath):
  319.                 return
  320.             destdir = os.path.dirname(destpath)
  321.             if not os.path.isdir(destdir):
  322.                 os.makedirs(destdir)
  323.             shutil.copy(path, destdir)
  324.  
  325.         @classmethod
  326.         def _install_exe(klass, exepath, root):
  327.             '''Install an executable and all linked shlibs into a chroot.'''
  328.  
  329.             klass._install_file(exepath, root)
  330.             ldd = subprocess.Popen(['ldd', exepath], stdout=subprocess.PIPE)
  331.             out = ldd.communicate()[0]
  332.             assert ldd.returncode == 0
  333.             for m in re.finditer(' => (/[^ ]+)', out):
  334.                 klass._install_file(m.group(1), root)
  335.             for m in re.finditer('^\s*(/[^ ]+)', out, re.M):
  336.                 klass._install_file(m.group(1), root)
  337.  
  338.         def test_shell_ops(self):
  339.             '''various shell operations in the chroot.'''
  340.  
  341.             d = tempfile.mkdtemp()
  342.             ldir = os.path.join(d, 'lib')
  343.             os.mkdir(ldir)
  344.             assert subprocess.call('cp -a /lib/*.so ' + ldir, shell=True) == 0
  345.             try:
  346.                 for cmd in ('bash', 'echo', 'cat', 'cp', 'ln', 'ls', 'rm',
  347.                     'mkdir', 'rmdir', 'chmod', 'chown'):
  348.                     self._install_exe('/bin/' + cmd, d)
  349.                 self._install_exe('/usr/bin/stat', d)
  350.  
  351.                 c = Chroot(d)
  352.  
  353.                 self.assertEqual(c.run_capture(['echo', 'hello']),
  354.                     ('hello\n', '', 0))
  355.                 self.assertEqual(c.run_capture(['cat'], 'hello'),
  356.                     ('hello', '', 0))
  357.                 self.assertEqual(c.run_capture(['/bin/bash'], 'type echo'),
  358.                     ('echo is a shell builtin\n', '', 0))
  359.                 self.assertEqual(c.run_capture(['/bin/bash'], 'set -e; false'),
  360.                     ('', '', 1))
  361.  
  362.                 # check ls
  363.                 (out, err, result) = c.run_capture(['ls'])
  364.                 self.assertEqual(err, '')
  365.                 self.assertEqual(result, 0)
  366.                 files = out.splitlines()
  367.                 self.assert_('bin' in files)
  368.                 self.assert_('lib' in files)
  369.  
  370.                 # complex shell commands: relative symlinks and paths
  371.                 self.assertEqual(c.run_capture(['bash'], '''set -e
  372. cd /
  373. mkdir test
  374. echo world > test/file
  375. ln -s file test/link
  376. cat test/file
  377. cat test/link
  378. stat -c '%f %s %n' test/file
  379. stat -c '%f %n' test/link
  380. stat -L -c '%f %s %n' test/link
  381. chmod 600 test/file
  382. chown 0:0 test/file
  383. stat -c '%f %s %n' test/file
  384. rm test/link
  385. rm test/file
  386. rmdir test
  387. mkdir test
  388. echo world > test/file
  389. rm -r test
  390. ! test -e test
  391. '''), ('world\nworld\n81a4 6 test/file\na1ff test/link\n81a4 6 test/link\n8180 6 test/file\n', '', 0))
  392.  
  393.                 # complex shell commands: relative symlink to executable
  394.                 self.assertEqual(c.run_capture(['bash'], '''set -e
  395. cd /
  396. ln -s echo bin/eco
  397. stat -c '%f %n' bin/eco
  398. stat -L -c '%f %n' bin/eco
  399. eco -n hello
  400. rm bin/eco
  401. '''), ('a1ff bin/eco\n81ed bin/eco\nhello', '', 0))
  402.  
  403.                 # complex shell commands: absolute symlinks and paths
  404.                 self.assertEqual(c.run_capture(['bash'], '''set -e
  405. mkdir /test
  406. echo world > /test/file
  407. ln -s /test/file /test/link
  408. cat /test/file
  409. cat /test/link
  410. stat -c '%f %s %n' /test/file
  411. stat -c '%f %n' /test/link
  412. stat -L -c '%f %s %n' /test/link
  413. chmod 600 /test/file
  414. chown 0:0 /test/file
  415. stat -c '%f %s %n' /test/file
  416. rm /test/link
  417. rm /test/file
  418. rmdir /test
  419. mkdir /test
  420. echo world > /test/file
  421. rm -r /test
  422. ! test -e test
  423. '''), ('world\nworld\n81a4 6 /test/file\na1ff /test/link\n81a4 6 /test/link\n8180 6 /test/file\n', '', 0))
  424.  
  425.                 # complex shell commands: absolute symlink to executable
  426.                 self.assertEqual(c.run_capture(['bash'], '''set -e
  427. ln -s /bin/echo /bin/eco
  428. /bin/eco -n hello
  429. rm /bin/eco
  430. '''), ('hello', '', 0))
  431.  
  432.                 # complex shell commands: cp/cat/rm of relative paths
  433.                 self.assertEqual(c.run_capture(['bash'], '''set -e
  434. cd /
  435. mkdir etc
  436. echo "/bin/bash" > etc/shells
  437. cp etc/shells etc/shells.tmp
  438. cat etc/shells.tmp
  439. rm etc/shells.tmp
  440. rm etc/shells
  441. rmdir etc
  442. '''), ('/bin/bash\n', '', 0))
  443.  
  444.                 # complex shell commands: cp/cat/rm of absolute paths
  445.                 self.assertEqual(c.run_capture(['bash'], '''set -e
  446. mkdir /etc
  447. echo "/bin/bash" > /etc/shells
  448. cp /etc/shells /etc/shells.tmp
  449. cat /etc/shells.tmp
  450. rm /etc/shells.tmp
  451. rm /etc/shells
  452. rmdir /etc
  453. '''), ('/bin/bash\n', '', 0))
  454.  
  455.             finally:
  456.                 shutil.rmtree(d)
  457.  
  458.     unittest.main()
  459.